package com.example.service.impl;

import com.example.component.ShortLinkComponent;
import com.example.config.RabbitMQConfig;
import com.example.constant.RedisKey;
import com.example.controller.request.*;
import com.example.enums.BizCodeEnum;
import com.example.enums.DomainTypeEnum;
import com.example.enums.EventMessageType;
import com.example.enums.ShortLinkStateEnum;
import com.example.feign.TrafficFeignService;
import com.example.interceptor.LoginInterceptor;
import com.example.manager.DomainManager;
import com.example.manager.GroupCodeMappingManager;
import com.example.manager.LinkGroupManager;
import com.example.manager.ShortLinkManager;
import com.example.model.*;
import com.example.mapper.ShortLinkMapper;
import com.example.service.ShortLinkService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.utils.CommonUtil;
import com.example.utils.IDUtil;
import com.example.utils.JsonData;
import com.example.utils.JsonUtil;
import com.example.vo.ShortLinkVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author yinxin
 * @since 2022-06-08
 */
@Service
@Slf4j
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    @Autowired
    private ShortLinkManager shortLinkManager;


    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitMQConfig rabbitMQConfig;

    @Autowired
    private DomainManager domainManager;

    @Autowired
    private LinkGroupManager linkGroupManager;

    @Autowired
    private ShortLinkComponent shortLinkComponent;

    @Autowired
    private GroupCodeMappingManager groupCodeMappingManager;

    @Autowired
    RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    TrafficFeignService trafficFeignService;

    @Override
    public ShortLinkVO parseShortLinkCode(String shortLinkCode) {
        ShortLinkDO shortLinkDO = shortLinkManager.findByShortLinkCode(shortLinkCode);
        if (shortLinkDO == null) {
            return null;
        }
        ShortLinkVO shortLinkVO = new ShortLinkVO();
        BeanUtils.copyProperties(shortLinkDO, shortLinkVO);
        return shortLinkVO;
    }


    //===============短链的增删改操作mq监听后的处理方法
    //冗余双写处理方法 B端额C端用户的短链信息双写
    @Override
    public Boolean handleAddShortLink(EventMessage eventMessage) {
        Long accountNo = eventMessage.getAccountNo();

        ShortLinkAddRequest shortLinkAddRequest = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkAddRequest.class);
        //短链域名校验
        DomainDO domainDO = checkDomain(shortLinkAddRequest.getDomainType(), shortLinkAddRequest.getDomainId(), accountNo);
        //校验组是否合法
        LinkGroupDO linkGroupDO = checkLinkGroup(shortLinkAddRequest.getGroupId(), accountNo);

        //长链摘要
        String originalUrlDigest = CommonUtil.MD5(shortLinkAddRequest.getOriginalUrl());

        String shortLinkCode = shortLinkComponent.createShortLinkCode(shortLinkAddRequest.getOriginalUrl());
        // =================加锁
        //是否重试
        Boolean duplicateCodeFlag = false;
        //key1是短链码，ARGV[1]是accountNo,ARGV[2]是过期时间
        String script =
                "if redis.call('EXISTS',KEYS[1])==0 " +
                        "then redis.call('set',KEYS[1],ARGV[1]); " +
                        "redis.call('expire',KEYS[1],ARGV[2]); " +
                        "return 1;" +
                        " elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" +
                        " else return 0; end;";
        Long lock = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class)
                , Arrays.asList(shortLinkCode), accountNo, 100);
        //先判断是否短链码被占用


        String messageType = eventMessage.getEventMessageType();

        if (lock > 0) {
            if (EventMessageType.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(messageType)) {
                //C端消费者
                ShortLinkDO shortLinkDOInDB = shortLinkManager.findByShortLinkCode(shortLinkCode);
                //短链码重复处理

                if (shortLinkDOInDB == null) {

                    //rpc调用 创建短链后要扣减短链包
                    boolean reduce = reduceTraffic(eventMessage,shortLinkCode);
                    log.info("扣减短链包:{}",reduce);

                    ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                            .accountNo(accountNo)
                            .code(shortLinkCode)
                            .title(shortLinkAddRequest.getTitle())
                            .originalUrl(shortLinkAddRequest.getOriginalUrl())
                            .domain(domainDO.getValue())
                            .groupId(linkGroupDO.getId())
                            .expired(shortLinkAddRequest.getExpired())
                            .sign(originalUrlDigest)
                            .state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();
                    shortLinkManager.addShortLink(shortLinkDO);
                    return true;
                } else {
                    log.error("B端短链码重复:{}", eventMessage);
                    duplicateCodeFlag = true;
                }
            } else if (EventMessageType.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(messageType)) {

                GroupCodeMappingDO groupCodeMappingDOInDB = groupCodeMappingManager.findByCodeAndGroupId(shortLinkCode, linkGroupDO.getId(), accountNo);

                //短链码重复处理
                if (groupCodeMappingDOInDB == null) {
                    //B端处理
                    GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
                            .accountNo(accountNo)
                            .code(shortLinkCode)
                            .title(shortLinkAddRequest.getTitle())
                            .originalUrl(shortLinkAddRequest.getOriginalUrl())
                            .domain(domainDO.getValue())
                            .groupId(linkGroupDO.getId())
                            .expired(shortLinkAddRequest.getExpired())
                            .sign(originalUrlDigest)
                            .state(ShortLinkStateEnum.ACTIVE.name())
                            .del(0)
                            .build();
                    groupCodeMappingManager.add(groupCodeMappingDO);
                    return true;
                } else {
                    log.error("C端短链码重复:{}", eventMessage);
                    duplicateCodeFlag = true;
                }
            }
        }else {
            //加锁失败重试
            log.info("加锁失败:{}",eventMessage);
            try {Thread.sleep(100);} catch (InterruptedException e) {}
            //是否重试
            duplicateCodeFlag = true;
        }

        if(duplicateCodeFlag){
            String newOriginalUrl = CommonUtil.addUrlPrefixVersion(shortLinkAddRequest.getOriginalUrl());
            shortLinkAddRequest.setOriginalUrl(newOriginalUrl);
            eventMessage.setContent(JsonUtil.obj2Json(shortLinkAddRequest));
            log.warn("短链码报错失败，重新生成:{}",eventMessage);
            handleAddShortLink(eventMessage);
        }
        return false;
    }

    /**
     * rpc调用扣减流量包
     * @param eventMessage
     * @param shortLinkCode
     * @return
     */
    private boolean reduceTraffic(EventMessage eventMessage, String shortLinkCode) {
        UseTrafficRequest request = UseTrafficRequest.builder()
                .accountNo(eventMessage.getAccountNo())
                .bizId(shortLinkCode).build();
        JsonData jsonData = trafficFeignService.useTraffic(request);
        if(jsonData.getCode()!=200){
            log.info("流量包不足，扣减失败:{}",eventMessage);
            return false;
        }
        return true;
    }

    @Override
    public Boolean handleDelShortLink(EventMessage eventMessage) {
        Long accountNo = eventMessage.getAccountNo();
        ShortLinkDelRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkDelRequest.class);

        String eventMessageType = eventMessage.getEventMessageType();
        if(EventMessageType.SHORT_LINK_DEL_LINK.name().equals(eventMessageType)){
            //C端解析
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .code(request.getCode())
                    .accountNo(accountNo)
                    .build();
            int rows = shortLinkManager.del(shortLinkDO);
            log.info("删除C端:{}",rows);
            return true;
        }else if(EventMessageType.SHORT_LINK_DEL_MAPPING.name().equals(eventMessageType)){
            //B端解析
            GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
                    .id(request.getMappingId())
                    .groupId(request.getGroupId())
                    .accountNo(accountNo)
                    .build();
            int rows = groupCodeMappingManager.del(groupCodeMappingDO);
            log.info("删除C端:{}",rows);
            return true;
        }
        return false;
    }

    @Override
    public Boolean handleUpdateShortLink(EventMessage eventMessage) {
        Long accountNo = eventMessage.getAccountNo();
        ShortLinkUpdateRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkUpdateRequest.class);
        String eventMessageType = eventMessage.getEventMessageType();
        //校验短链域名
        DomainDO domainDO = checkDomain(request.getDomainType(), request.getDomainId(), accountNo);

        //组名在添加的时候已经校验了所以这里不需要校验

        if(EventMessageType.SHORT_LINK_UPDATE_LINK.name().equalsIgnoreCase(eventMessageType)){
            //C端处理
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .code(request.getCode())
                    .title(request.getTitle())
                    .domain(domainDO.getValue())
                    .accountNo(accountNo)
                    .build();
            int rows = shortLinkManager.update(shortLinkDO);
            log.info("跟新B端短链,rows:{}",rows);
            return true;
        }else if(EventMessageType.SHORT_LINK_UPDATE_MAPPING.name().equalsIgnoreCase(eventMessageType)){
            //B端处理
            GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
                    .id(request.getMappingId())
                    .groupId(request.getGroupId())
                    .accountNo(accountNo)
                    .title(request.getTitle())
                    .domain(domainDO.getValue())
                    .build();
            int rows = groupCodeMappingManager.update(groupCodeMappingDO);
            log.info("跟新C端短链,rows:{}",rows);
            return true;
        }
        return false;
    }

    /**
     * 分页查找短链
     */
    @Override
    public JsonData pageByGroupId(ShortLinkPageRequest request) {
        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

        Map<String, Object> map = groupCodeMappingManager.pageShortLinkByGroupId(request.getPage()
                , request.getSize()
                , accountNo, request.getGroupId());
        return JsonData.buildSuccess(map);
    }

    //==========短链的增删改操作，并发送到mq中等待处理
    @Override
    public JsonData createShortLink(ShortLinkAddRequest request) {
        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

        log.info("获取到的当前用户的accountNo:{}", accountNo);

        //预先检测下是否有足够多的可以进行创建
        String cacheKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC, accountNo);
        //检查key是否存在，递减，是否大于等于0，使用lua脚本
        //如果key不存在，则未使用过，lua返回值是0；新增流量包的时候，不用重新计算次数，直接删除key，消费的时候会计算更新值
        String script =
                "if redis.call('get',KEYS[1]) then return redis.call('decr',KEYS[1]) else return 0 end;";

        Long leftTimes = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class)
                , Arrays.asList(cacheKey), "");
        log.info("今日流量包剩余次数:{}",leftTimes);

        if(leftTimes>=0){
            String newOriginalUrl = CommonUtil.addUrlPrefix(request.getOriginalUrl());
            request.setOriginalUrl(newOriginalUrl);

            EventMessage eventMessage = EventMessage.builder()
                    .accountNo(accountNo)
                    .content(JsonUtil.obj2Json(request))
                    .messageId(IDUtil.geneSnowFlakeID()+"")
                    .eventMessageType(EventMessageType.SHORT_LINK_ADD.name())
                    .build();
            log.info("新增传送到mq中的消息EventMessage:{}", eventMessage);
            rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange()
                    , rabbitMQConfig.getShortLinkAddRoutingKey()
                    , eventMessage);
            return JsonData.buildSuccess();
        }else {
            //流量包不足
            return JsonData.buildResult(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
        }


    }

    @Override
    public JsonData del(ShortLinkDelRequest request) {
        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        log.info("获取到的当前用户的accountNo:{}", accountNo);
        EventMessage eventMessage = EventMessage.builder()
                .accountNo(accountNo)
                .content(JsonUtil.obj2Json(request))
                .messageId(IDUtil.geneSnowFlakeID() + "")
                .eventMessageType(EventMessageType.SHORT_LINK_DEL.name())
                .build();
        log.info("删除传送到mq中的消息EventMessage:{}", eventMessage);
        rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange()
                , rabbitMQConfig.getShortLinkDelRoutingKey()
                , eventMessage);
        return JsonData.buildSuccess();
    }

    @Override
    public JsonData update(ShortLinkUpdateRequest request) {
        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        log.info("获取到的当前用户的accountNo:{}", accountNo);
        EventMessage eventMessage = EventMessage.builder()
                .accountNo(accountNo)
                .content(JsonUtil.obj2Json(request))
                .messageId(IDUtil.geneSnowFlakeID() + "")
                .eventMessageType(EventMessageType.SHORT_LINK_UPDATE.name())
                .build();
        log.info("跟新传送到mq中的消息EventMessage:{}", eventMessage);
        rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange()
                , rabbitMQConfig.getShortLinkUpdateRoutingKey()
                , eventMessage);
        return JsonData.buildSuccess();
    }

    //================================抽离出来的逻辑方法=====================================

    /**
     * 校验域名
     *
     * @param domainType
     * @param domainId
     * @param accountNo
     * @return
     */
    private DomainDO checkDomain(String domainType, Long domainId, Long accountNo) {

        DomainDO domainDO;
        if (DomainTypeEnum.CUSTOM.name().equals(domainType)) {
            //自建
            domainDO = domainManager.findById(domainId, accountNo);
        } else {
            //官方
            domainDO = domainManager.findByDomainTypeAndID(domainId, DomainTypeEnum.OFFICAL);
        }
        Assert.notNull(domainDO, "短链域名不合法");
        return domainDO;
    }

    /**
     * 验证组名
     * @param groupId
     * @param accountNo
     * @return
     */
    private LinkGroupDO checkLinkGroup(Long groupId, Long accountNo) {
        LinkGroupDO linkGroupDO = linkGroupManager.detail(groupId, accountNo);
        Assert.notNull(linkGroupDO, "组名不合法");
        log.info("组名不合法");
        return linkGroupDO;
    }

}
